unit CPView;

interface

uses CPBase, CPPlotCanvas;

const
  { Miminum value of Y Scale ratio }
  MinYScale = 1E-9;
  { Maxinum value of Y Scale ratio }
  MaxYScale = 1E9;

type
  { Describes a rectangular viewing region on a 2D coordinate system.
    This class provides routines for transforming coordinates to
    canvas units. If the canvas is a bitmap then these units
    are pixels, if the canvas is a vector graphic these units could be
    milimeter. }
  TCPView = class
  private
    procedure SetYAxisScale(const Value: TCPFloat);
  protected
    { Scalation of coordinate system. This describes the size of
      one coordinate unit in canvas units. }
    FScale: TCPVector;
    { Translation of coordinate system. This describes the translation of
    the coordinate system in canvas units. }
    FTranslation: TCPVector;
    { Scale ratio of Y-axis compared to X-axis. }
    FYAxisScale: TCPFloat;
    { Scaling of the view expressed as binary logartihm. Increasing the
      zoom by 1 will scale up the coordinate system by 2. }
    FZoom: TCPFloat;
    { View is centered on these coordinates. }
    FCenter: TCPPoint;
    { Canvas that is plotted on. Owned by this instance. }
    FPlotCanvas: TCPPlotCanvas;
    { Top left and bottom right coordinates of view }
    FViewRect: TCPRect;
    { Returns horizontal scale of view }
    function GetViewScale: TCPFloat;
    { Is called, after the view has been changed. }
    procedure ViewUpdated; overload; virtual;
    { Calculates the new view on the coordinate system and repaints it.
      @param Zoom New zoom value to use
      @param Center New view center
      @param YAxisRatio New Y axis scale ratio }
    procedure ViewUpdated(Zoom: TCPFloat; Center:TCPVector;
      YAxisRatio: TCPFloat); overload;
    { Called, when plot canvas is changed }
    procedure PlotCanvasChanged; virtual;
    { Sets a new plotting canvas. }
    procedure SetPlotCanvas(const Value: TCPPlotCanvas);
    { Calculates the transformation bounds of the canvas for converting
      coordinate units to canvas units. If the canvas requires mirroring of
      Y-coordinates, the height will be set to negative value and the vertical
      position will be aligned to the bottom. }
    function GetCanvasTransform: TCPBounds;
    { Ensures that both zoom and center values will be in range to express
      a valid view. }
    procedure EnsureViewRange(var AZoom: TCPFloat; var ACenter: TCPPoint);
  public
    constructor Create;
    destructor Destroy; override;
    { Assigns the specified view }
    procedure Assign(View: TCPView);
    { Convert view scale to zoom factor. }
    class function ScaleToZoom(Scale: TCPFloat): TCPFloat;
    { Convert zoom factor to view scale. }
    class function ZoomToScale(Zoom: TCPFloat): TCPFloat;
    { Transforms coordinates to canvas units. This is the
      inverse operation of ToCoords(). }
    function ToCanvas(const Coords: TCPPoint): TCPPoint;
    { Transforms a point from coordinate units to canvas units. This is the
      inverse operation of ToCanvas(). }
    function ToCoord(const Point: TCPPoint): TCPPoint;
    { Transforms a vector from coordinate units to canvas units. }
    function VectorToCoord(const Vector: TCPVector): TCPVector;
    { Adjust view so the left and right interval borders (defined in coordinate
      units) will mark the left and right screen border. YCoord value is
      used for the vertical alignment of the view.
      @param LeftBorder X-coordinate of left border
      @param RightBorder Y-coordinate of right border
      @param YCoord Vertical translation of the view}
    procedure SetViewBorders(LeftBorder, RightBorder: TCPFloat;
      TranslationY: TCPFloat);
    { Sets view parameters directly. }
    procedure ChangeView(Zoom: TCPFloat; YAxisScale: TCPFloat;
      Center: TCPPoint); virtual;
    { Reset view to the default view. }
    procedure ResetView; virtual;
    { Changes scalation, so the specified coordinates will not change
      their screen coordinates }
    procedure ZoomTo(Coords: TCPPoint; Zoom: TCPFloat); virtual;
    { Center view on a point of the cooridnate system. }
    procedure PanTo(Center: TCPPoint); virtual;
    { Changes zoom by ZoomDelta * FZoomStep. }
    procedure ZoomView(ZoomDelta: TCPFloat); virtual;
    { Moves the view center by PanDelta * FPanStep. }
    procedure PanView(PanFactor: TCPVector); virtual;
    { Updates canvas bounds
      @param Bounds Canvas bounds in canvas coordinates }
    procedure SetCanvasBounds(Bounds: TCPBounds);
    { Horizontal and vertical scale of view }
    property ViewScale: TCPFloat read GetViewScale;
    { Corner coordinates of the view region }
    property ViewRect: TCPRect read FViewRect;
    { Canvas which is plotted on }
    property PlotCanvas: TCPPlotCanvas read FPlotCanvas write SetPlotCanvas;
    { Center of view region }
    property Center: TCPPoint read FCenter;
    { Zoom of view region }
    property Zoom: TCPFloat read FZoom;
    { Ratio of Y-axis to X-axis }
    property YAxisScale: TCPFloat read FYAxisScale write SetYAxisScale;
  end;

  { Provides animated viewing region changes }
  TCPAnimatedView = class(TCPView);

implementation

uses Math, SysUtils, CPPlotExport, Classes;

const
  { Natural Logarithm of 0.5 }
  Ln0_5 = -0.69314718055994530941723212145818;

{ TCPView }

procedure TCPView.PanTo(Center: TCPPoint);
begin
  ChangeView(FZoom, FYAxisScale, Center);
end;

procedure TCPView.ResetView;
begin
  ChangeView(-2, FYAxisScale, CPToVector(0, 0));
end;

procedure TCPView.SetViewBorders(LeftBorder, RightBorder,
  TranslationY: TCPFloat);
begin
  ChangeView(ScaleToZoom(RightBorder - LeftBorder), FYAxisScale,
    CPToVector((RightBorder + LeftBorder) / 2, TranslationY));
end;

procedure TCPView.ChangeView(Zoom: TCPFloat; YAxisScale: TCPFloat;
  Center: TCPPoint);
begin
  EnsureViewRange(Zoom, Center);
  FZoom := Zoom;
  FYAxisScale := YAxisScale;
  FCenter := Center;
  ViewUpdated;
end;

function TCPView.ToCoord(const Point: TCPPoint): TCPPoint;
begin
  Result.X := (Point.X - FTranslation.X) / FScale.X;
  Result.Y := (Point.Y - FTranslation.Y) / FScale.Y;
end;

function TCPView.ToCanvas(const Coords: TCPPoint): TCPPoint;
begin
  Result.X := FScale.X * Coords.X + FTranslation.X;
  Result.Y := FScale.Y * Coords.Y + FTranslation.Y;
end;

function TCPView.VectorToCoord(const Vector: TCPVector): TCPVector;
begin
  Result.X := Vector.X / FScale.X;
  Result.Y := Vector.Y / FScale.Y;
end;


procedure TCPView.ZoomTo(Coords: TCPPoint; Zoom: TCPFloat);
var
  Translation2, Center2, Scale2: TCPPoint;
  B: TCPBounds;
begin
 {
  FScale.X = B.Width / ZoomToScale(Zoom)
  FTranslation = - FCenter * FScale + B.Size / 2 + B.Position

  FScale2.X = B.Width / ZoomToScale(Zoom2)
  FTranslation2 = - FCenter2 * FScale2 + B.Size / 2 + B.Position
  -> FCenter2 = (B.Size / 2 + B.Position - FTranslation2) / FScale2

  FScale * Coords + FTranslation = FScale2 * Coords + FTranslation2
  -> (FScale  - FScale2) * Coords + FTranslation = FTranslation2

  -> FTranslation2 = (FScale  - FScale2) * Coords + FTranslation
  -> FCenter2 = (B.Size / 2 + B.Position - FTranslation2) / FScale2
  }
  B := GetCanvasTransform;
  EnsureViewRange(Zoom, FCenter);
  Scale2.X := B.Width / ZoomToScale(Zoom);
  Scale2.Y := Sign(B.Height) * Scale2.X * FYAxisScale;
  Translation2 := CPAdd(CPMul(CPSub(FScale, Scale2), Coords), FTranslation);
  Center2.X := (B.Width / 2 + B.Left - Translation2.X) / Scale2.X;
  Center2.Y := (B.Height / 2 + B.Top - Translation2.Y) / Scale2.Y;
  ChangeView(Zoom, FYAxisScale, Center2);
end;

class function TCPView.ScaleToZoom(Scale: TCPFloat): TCPFloat;
begin
  Result := Ln(Scale) / Ln0_5; // = Log2(1/Scale)
end;

class function TCPView.ZoomToScale(Zoom: TCPFloat): TCPFloat;
begin
  Result := Exp(Ln0_5 * Zoom); // = 1/2^Zoom
end;

procedure TCPView.PanView(PanFactor: TCPVector);
var
  PanDelta: TCPVector;
begin
  PanDelta := CPMul(PanFactor,
    CPSub(FViewRect.BottomRight, FViewRect.TopLeft));
  ChangeView(FZoom, FYAxisScale, CPAdd(FCenter, PanDelta));
end;

procedure TCPView.ZoomView(ZoomDelta: TCPFloat);
begin
  ZoomTo(FCenter, FZoom + ZoomDelta);
end;

constructor TCPView.Create;
begin
  FYAxisScale := 1.0;
  FCenter := CPToVector(0.0, 0.0);
  FZoom := -2.0;
end;

destructor TCPView.Destroy;
begin
  FPlotCanvas.Free;
  inherited;
end;

procedure TCPView.SetPlotCanvas(const Value: TCPPlotCanvas);
begin
  if (Value <> FPlotCanvas) then
  begin
    FPlotCanvas.Free;
    FPlotCanvas := Value;
    PlotCanvasChanged;
  end;
end;

procedure TCPView.SetCanvasBounds(Bounds: TCPBounds);
begin
  if (Assigned(FPlotCanvas)) then
  begin
    if not (FPlotCanvas is TCPPlotExport) then
      FPlotCanvas.Bounds := Bounds;
    ViewUpdated;
  end;
end;

function TCPView.GetViewScale: TCPFloat;
begin
  Result := ZoomToScale(FZoom);
end;

function TCPView.GetCanvasTransform: TCPBounds;
begin
  if (Assigned(FPlotCanvas)) then
  begin
    Result := FPlotCanvas.Bounds;
    if (FPlotCanvas.MirrorYAxis) then
    begin
      Result.Top := Result.Height + Result.Top;
      Result.Height := -Result.Height;
    end;
  end
  else
    raise EInvalidOperation.Create('View has no plot canvas attached.');
end;

procedure TCPView.ViewUpdated(Zoom: TCPFloat; Center:TCPVector;
  YAxisRatio: TCPFloat);
var
  AViewScale: TCPVector;
  HalfViewSpan: TCPVector;
  B: TCPBounds;
begin
  if (FPlotCanvas = nil) then
    exit;
  // Calculate scale and translation values to normalize X-coordinates inside
  // the view region to [0; 1]
  B := GetCanvasTransform;
  AViewScale.X := ZoomToScale(Zoom);
  AViewScale.Y := AViewScale.X / YAxisScale;
  // Stretch normalized coordinates to canvas bounds
  FScale.X := B.Width / AViewScale.X;
  FScale.Y := Sign(B.Height) * B.Width / AViewScale.Y; // Apply same scaling to X & Y
  FTranslation.X := - Center.X * FScale.X + B.Width / 2 + B.Left;
  FTranslation.Y := - Center.Y * FScale.Y + B.Height / 2 + B.Top;
  // Update view bounds:
  HalfViewSpan.X := AViewScale.X / 2;
  HalfViewSpan.Y := AViewScale.Y * B.Height / B.Width / 2;
  FViewRect.Left := Center.X - HalfViewSpan.X;
  FViewRect.Top := Center.Y - HalfViewSpan.Y;
  FViewRect.Right := Center.X + HalfViewSpan.X;
  FViewRect.Bottom := Center.Y + HalfViewSpan.Y;
  // Make sure top left coordinates are always smaller then
  // bottom right coordinates:
  if (FScale.Y < 0) then
    CPSwap(FViewRect.Top, FViewRect.Bottom);
end;

procedure TCPView.ViewUpdated;
begin
  ViewUpdated(FZoom, FCenter, FYAxisScale);
end;

procedure TCPView.PlotCanvasChanged;
begin
  ViewUpdated;
end;

procedure TCPView.SetYAxisScale(const Value: TCPFloat);
begin
  if (Value <> FYAxisScale) then
  begin
    FYAxisScale := Value;
    ChangeView(FZoom, FYAxisScale, FCenter);
  end;
end;

procedure TCPView.Assign(View: TCPView);
begin
  ChangeView(View.FZoom, View.FYAxisScale, View.FCenter);
end;

procedure TCPView.EnsureViewRange(var AZoom: TCPFloat; var ACenter: TCPPoint);
begin
  AZoom := EnsureRange(-24, AZoom, 20);
  ACenter.X := EnsureRange(-1E8, ACenter.X, 1E8);
  ACenter.Y := EnsureRange(-1E8, ACenter.Y, 1E8);
end;

end.

